/* @flow */

import React, { Component } from 'react';

/**
 * The type of the React {@code Component} props of {@link Video}.
 */
type Props = {

    /**
     * CSS classes to add to the video element.
     */
    className: string,

    /**
     * The value of the id attribute of the video. Used by the torture tests to
     * locate video elements.
     */
    id: string,

    /**
     * Optional callback to invoke once the video starts playing.
     */
    onVideoPlaying?: Function,

    /**
     * The JitsiLocalTrack to display.
     */
    videoTrack: ?Object,

    /**
     * Used to determine the value of the autoplay attribute of the underlying
     * video element.
     */
    autoPlay: boolean,

    /**
     * Used to determine the value of the autoplay attribute of the underlying
     * video element.
     */
    playsinline: boolean,

    /**
     * A map of the event handlers for the video HTML element.
     */
    eventHandlers?: {|

        /**
         * OnAbort event handler.
         */
        onAbort?: ?Function,

        /**
         * OnCanPlay event handler.
         */
        onCanPlay?: ?Function,

        /**
         * OnCanPlayThrough event handler.
         */
        onCanPlayThrough?: ?Function,

        /**
         * OnEmptied event handler.
         */
        onEmptied?: ?Function,

        /**
         * OnEnded event handler.
         */
        onEnded?: ?Function,

        /**
         * OnError event handler.
         */
        onError?: ?Function,

        /**
         * OnLoadedData event handler.
         */
        onLoadedData?: ?Function,

        /**
         * OnLoadedMetadata event handler.
         */
        onLoadedMetadata?: ?Function,

        /**
         * OnLoadStart event handler.
         */
        onLoadStart?: ?Function,

        /**
         * OnPause event handler.
         */
        onPause?: ?Function,

        /**
         * OnPlay event handler.
         */
        onPlay?: ?Function,

        /**
         * OnPlaying event handler.
         */
        onPlaying?: ?Function,

        /**
         * OnRateChange event handler.
         */
        onRateChange?: ?Function,

        /**
         * OnStalled event handler.
         */
        onStalled?: ?Function,

        /**
         * OnSuspend event handler.
         */
        onSuspend?: ?Function,

        /**
         * OnWaiting event handler.
         */
        onWaiting?: ?Function
    |},

    /**
     * A styles that will be applied on the video element.
     */
    style?: Object,

    /**
     * The value of the muted attribute for the underlying video element.
     */
    muted?: boolean
};

/**
 * Component that renders a video element for a passed in video track.
 *
 * @augments Component
 */
class Video extends Component<Props> {
    _videoElement: ?Object;
    _mounted: boolean;

    /**
     * Default values for {@code Video} component's properties.
     *
     * @static
     */
    static defaultProps = {
        className: '',
        autoPlay: true,
        id: '',
        playsinline: true
    };

    /**
     * Initializes a new {@code Video} instance.
     *
     * @param {Object} props - The read-only properties with which the new
     * instance is to be initialized.
     */
    constructor(props: Props) {
        super(props);

        /**
         * The internal reference to the DOM/HTML element intended for
         * displaying a video.
         *
         * @private
         * @type {HTMLVideoElement}
         */
        this._videoElement = null;


        // Bind event handlers so they are only bound once for every instance.
        this._onVideoPlaying = this._onVideoPlaying.bind(this);
        this._setVideoElement = this._setVideoElement.bind(this);
    }

    /**
     * Invokes the library for rendering the video on initial display. Sets the
     * volume level to zero to ensure no sound plays.
     *
     * @inheritdoc
     * @returns {void}
     */
    componentDidMount() {
        this._mounted = true;

        if (this._videoElement) {
            this._videoElement.volume = 0;
            this._videoElement.onplaying = this._onVideoPlaying;
        }

        this._attachTrack(this.props.videoTrack);

        if (this._videoElement && this.props.autoPlay) {
            // Ensure the video gets play() called on it. This may be necessary in the
            // case where the local video container was moved and re-attached, in which
            // case video does not autoplay.
            this._videoElement.play()
                .catch(error => {
                    // Prevent uncaught "DOMException: The play() request was interrupted by a new load request"
                    // when video playback takes long to start and it starts after the component was unmounted.
                    if (this._mounted) {
                        throw error;
                    }
                });
        }
    }

    /**
     * Remove any existing associations between the current video track and the
     * component's video element.
     *
     * @inheritdoc
     * @returns {void}
     */
    componentWillUnmount() {
        this._mounted = false;
        this._detachTrack(this.props.videoTrack);
    }

    /**
     * Updates the video display only if a new track is added. This component's
     * updating is blackboxed from React to prevent re-rendering of video
     * element, as the lib uses {@code track.attach(videoElement)} instead.
     *
     * @inheritdoc
     * @returns {boolean} - False is always returned to blackbox this component
     * from React.
     */
    shouldComponentUpdate(nextProps: Props) {
        const currentJitsiTrack = this.props.videoTrack
            && this.props.videoTrack.jitsiTrack;
        const nextJitsiTrack = nextProps.videoTrack
            && nextProps.videoTrack.jitsiTrack;

        if (currentJitsiTrack !== nextJitsiTrack) {
            this._detachTrack(this.props.videoTrack);
            this._attachTrack(nextProps.videoTrack);
        }

        if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
            return true;
        }

        return false;
    }

    /**
     * Renders the video element.
     *
     * @override
     * @returns {ReactElement}
     */
    render() {
        const {
            autoPlay,
            className,
            id,
            muted,
            playsinline,
            style,
            eventHandlers
        } = this.props;

        return (
            <video
                autoPlay = { autoPlay }
                className = { className }
                id = { id }
                muted = { muted }
                playsInline = { playsinline }
                ref = { this._setVideoElement }
                style = { style }
                { ...eventHandlers } />
        );
    }

    /**
     * Calls into the passed in track to associate the track with the
     * component's video element and render video.
     *
     * @param {Object} videoTrack - The redux representation of the
     * {@code JitsiLocalTrack}.
     * @private
     * @returns {void}
     */
    _attachTrack(videoTrack) {
        if (!videoTrack || !videoTrack.jitsiTrack) {
            return;
        }

        videoTrack.jitsiTrack.attach(this._videoElement);
    }

    /**
     * Removes the association to the component's video element from the passed
     * in redux representation of jitsi video track to stop the track from
     * rendering.
     *
     * @param {Object} videoTrack -  The redux representation of the
     * {@code JitsiLocalTrack}.
     * @private
     * @returns {void}
     */
    _detachTrack(videoTrack) {
        if (this._videoElement && videoTrack && videoTrack.jitsiTrack) {
            videoTrack.jitsiTrack.detach(this._videoElement);
        }
    }

    _onVideoPlaying: () => void;

    /**
     * Invokes the onvideoplaying callback if defined.
     *
     * @private
     * @returns {void}
     */
    _onVideoPlaying() {
        if (this.props.onVideoPlaying) {
            this.props.onVideoPlaying();
        }
    }

    _setVideoElement: () => void;

    /**
     * Sets an instance variable for the component's video element so it can be
     * referenced later for attaching and detaching a JitsiLocalTrack.
     *
     * @param {Object} element - DOM element for the component's video display.
     * @private
     * @returns {void}
     */
    _setVideoElement(element) {
        this._videoElement = element;
    }
}

export default Video;
